package nkr1pt.popkorn.events;

import com.google.inject.Singleton;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import nkr1pt.popkorn.events.EventObserver;

/**
 * Some kind of Mediator for global event notification.
 * Observers can subscribe here to receive specific events.
 * Publishers will fire events on this bus, which on its turn will notify all
 * observers who have subscribed to be notified of these events.
 *
 * @author Kristof Vanhaeren
 */
@Singleton
public class EventBus {

    private static final Logger logger = Logger.getLogger(EventBus.class.getName());
    private Map<EventObserver, Set<PopkornEvent.EventType>> observerToEventBindings;
    private Map<PopkornEvent.EventType, SortedSet<ObserverPriorityTuple>> eventPriorityBindings;

    public EventBus() {
        observerToEventBindings = new HashMap<EventObserver, Set<PopkornEvent.EventType>>();
        eventPriorityBindings = new HashMap<PopkornEvent.EventType, SortedSet<ObserverPriorityTuple>>();
    }

    /**
     * observer will be notified when one or more events of the specified event classes are fired.
     *
     * @param observer
     * @param events
     */
    public void subscribe(EventObserver observer, Set<PopkornEvent.EventType> eventTypes) {
        observerToEventBindings.put(observer, eventTypes);
    }

    /**
     * observer will be notified when an event of the specified type is fired.
     *
     * @param observer
     * @param eventClass
     */
    public void subscribe(EventObserver observer, PopkornEvent.EventType eventType) {
        if (observerToEventBindings.containsKey(observer)) {
            observerToEventBindings.get(observer).add(eventType);
        } else {
            Set<PopkornEvent.EventType> eventTypes = new HashSet<PopkornEvent.EventType>();
            eventTypes.add(eventType);
            observerToEventBindings.put(observer, eventTypes);
        }
    }

    public void addEventPriorityBinding(PopkornEvent.EventType eventType, EventObserver observer, int priority) {
        SortedSet<ObserverPriorityTuple> sortedSet = null;
        if (eventPriorityBindings.containsKey(eventType)) {
            sortedSet = eventPriorityBindings.get(eventType);
        } else {
            sortedSet = new TreeSet(new PriorityComparator());
        }

        sortedSet.add(new ObserverPriorityTuple(observer, priority));
        eventPriorityBindings.put(eventType, sortedSet);
    }

    public void fireEvent(PopkornEvent event) {
       logger.log(Level.INFO, event.getType().toString() + " event fired by " + event.getSource());
        if (event.hasException()) {
            logger.log(Level.INFO, "This event holds an exception with the message '" + event.getException().getMessage() + "'");
        }

        for (Map.Entry<PopkornEvent.EventType, SortedSet<ObserverPriorityTuple>> me : eventPriorityBindings.entrySet()) {
            if (me.getKey().equals(event.getType())) {
                for (ObserverPriorityTuple tuple : me.getValue()) {
                    tuple.getObserver().handleEvent(event);
                    for (Map.Entry<EventObserver, Set<PopkornEvent.EventType>> me2 : observerToEventBindings.entrySet()) {
                        if (me2.getKey().equals(tuple.getObserver()) && me2.getValue().contains(event.getType())) {
                            me2.getValue().remove(event.getType());
                        }
                    }
                }
            }
        }

        for (Map.Entry<EventObserver, Set<PopkornEvent.EventType>> me : observerToEventBindings.entrySet()) {
            for (PopkornEvent.EventType eventType : me.getValue()) {
                if (eventType.equals(event.getType())) {
                    me.getKey().handleEvent(event);
                }
            }
        }
    }

    private class ObserverPriorityTuple {

        private EventObserver observer;
        private Integer priority;

        public ObserverPriorityTuple(EventObserver observer, Integer priority) {
            this.observer = observer;
            this.priority = priority;
        }

        public EventObserver getObserver() {
            return observer;
        }

        public void setObserver(EventObserver observer) {
            this.observer = observer;
        }

        public Integer getPriority() {
            return priority;
        }

        public void setPriority(Integer priority) {
            this.priority = priority;
        }
    }

    private class PriorityComparator implements Comparator<ObserverPriorityTuple> {

        public int compare(ObserverPriorityTuple o1, ObserverPriorityTuple o2) {
            if (o1.getPriority() > o2.getPriority()) {
                return 1;
            } else if (o1.getPriority() < o2.getPriority()) {
                return -1;
            } else {
                return 0;
            }
        }
    }
}